/*
 *	CODESPACE ++
 *	A real-time syntax highlighting javascript editor
 *
 *	Copyright (C) 2008 Francois Botman, <Franchie.brightspark@gmail.com>
 *
 *	This script is licenced under: Creative Commons Attribution-Noncommercial-Share Alike 3.0
 *	(see http://creativecommons.org/licenses/by-nc-sa/3.0/).
 *	It bascially means that it can be modified and shared, provided it is
 *	clearly attributed, and can be used in personnal/charity projects for free!
 *	(Commercial use requires prior negotiation with the author).
 *	In any case, should anything go wrong, I cannot be held responsible!
 *
 *	Based on an original idea by Fernando M.A.d.S.,  http://codepress.org/
 *
 *	-----------
 *
 *	This file contains the LINE object that contains all the information about a line.
 *	It is a doubly linked list: when a line is deleted, all the following lines are automatically updated.
 *	This file only handles:
 *		- curson display/update
 *		- selection overlay
 *		- selection erasure procedures
 */

Codespace.Line = function(
							line,			// 0-based index of the line this LINE represents
							text,			// Initial text value of the line
							next, 			// Pointer to the next LINE
							prev,			// Pointer to the previous LINE
							editor,			// Pointer to the editor object containing like loads of data!
							preventUpdate	// [optional] prevents the syntax highlighter from activating
						) {

	var self_line = this;
	this.line	= line;
	this.next	= next;
	this.prev	= prev;
	this.table	= editor.table;
	this.cell	= createTableRow(line);
	this.words	= new Array();
	this.preventUpdate = preventUpdate;
	
	

	
	/*
	 *	This creates the cells for each row according to the options.
	 */
	function createTableRow(line) {
		var row = self_line.table.insertRow(line);
		row.lineobj = self_line;

		// The line numbers
		if(editor.options.showLineNumbers) {
			var a = row.insertCell((editor.options.lineNumberBar>row.cells.length) ? row.cells.length : editor.options.lineNumberBar);
			a.className = 'codespace linenumberbar';
			a.vAlign = 'top';
		}
		
		// The code folding bar
		if(editor.options.showFolding) {
			var b = row.insertCell((editor.options.foldingBar>row.cells.length) ? row.cells.length : editor.options.foldingBar);
			b.className = 'codespace foldingbar';
			b.innerHTML = '&nbsp;&nbsp;';
		}
		
		// And the actual textual content
		var c = row.insertCell((editor.options.contentBar>row.cells.length) ? row.cells.length : editor.options.contentBar);
		c.className = 'codespace contentbar';
		return c;
	}
	
	// A destroy function. Changes links, and modifies line numbers of following lines
	this.destroy = function(update) {
		editor.table.removeChild(editor.table.rows[self_line.line]);
		if(update) {
			if(self_line.prev) self_line.prev.next = self_line.next;
			if(self_line.next) self_line.next.prev = self_line.prev;
			self_line.next.updateLineNumber(-1);
		}
	}
	
	// And a convenience function...
	this.insertAfter = function() {
		// First, transfer the rest of this line to the next (the half-word is catered for later)
		var txt = '';
	
		var o = self_line.words.indexOf( editor.getNextWord( editor.caret.obj ) );
		var a = new Codespace.Line(self_line.line+1, '', self_line.next, self_line, editor, true);
		
		if(o>-1) {
			var removed = self_line.words.splice(o, self_line.words.length - o);
			var q;
			var last = a.words[0];
			a.words = a.words.concat(removed);
			while(q=removed.shift()) {
				q.lineobj = a;
				a.cell.appendChild(q);
			}
			if(last.text) Codespace.Formatter(last, (last == a.words[ a.words.length - 1]) ? true : false, editor.options.language);
		}

		if(self_line.words.length==1 && !self_line.words[0].text) self_line.words[0].innerHTML = '&nbsp;';
		
		// And update the lines...
		a.preventUpdate = false;
		a.updateHighlight( [].concat( self_line.words[ self_line.words.length-1 ], a.words ) );
		
		// And move the caret
		editor.caret.obj = a.words[0];
		editor.caret.objOffset = 0;

		if(self_line.next) self_line.next.prev = a;
		self_line.next = a;
		if(a.next) a.next.updateLineNumber(+1);
		return a;
	}
	
	// Update the line numbers (delta variation). Is recursive!
	this.updateLineNumber = function(delta) {
		if(!editor.options.showLineNumbers) return;
		if(delta) {
			self_line.line = self_line.line + delta;
			if(self_line.next) self_line.next.updateLineNumber(delta);
		}
		//self_line.table.lineNumberBar.innerHTML = self_line.line + '.';
		self_line.table.rows[self_line.line].cells[editor.options.lineNumberBar].innerHTML = (self_line.line+1) + '.';
	}
	
	// Updates the code folding column
	this.updateFolding = function() {
		//if(Codespace.Folder) Codespace.Folder(self_line, self_line.prev, self_line.table.foldingBar);
	}
	
	// Inserts text at current caret position
	this.insertText = function(text) {
		// TODO: handle what happens during a selection...
		
		var w = Codespace.Formatter.split(text, editor.options.language);
		var element = editor.caret.obj;
		//var update_element_start = element;
		var original_length = w.length;
		var updated_elements = [];
		
		// Eliminate "empty" items... these can case format problems at a later stage...
		/*if(self_line.words.length>1 && !self_line.words[ self_line.words.length - 1].text) {
			var a = self_line.words[ self_line.words.length - 1];
			self_line.words.remove( a );
			a.parentNode.removeChild(a);
		}*/
		
		// Loop on all word segments that need to be processed...
		for(var i=0; i<w.length; i++) {
		
			// And add a new element if necessary...
			if(i<(w.length-1) || !w[i] || editor.caret.obj.parentNode != self_line.cell) {
				var p = self_line.words.indexOf(element);
				if(self_line.words.length && p>-1 && !self_line.words[ p ].text) element = self_line.words[ p ];
				else {
					var a = document.createElement('span');
					if(element && element.nextSibling && element.parentNode == self_line.cell) self_line.cell.insertBefore( a, element.nextSibling );
					else self_line.cell.appendChild(a);
					// We are inserting a new element *inside* the contents of an existing one.
					if(element && editor.caret.objOffset != (element.text ? element.text.length : 0))  {
						var x = element.text.slice(0, editor.caret.objOffset);
						var y = element.text.slice(editor.caret.objOffset, element.text.length);
						element.text = x ? x : '';
						w.push( y );
					}
					if(!self_line.words.length) a.innerHTML = '&nbsp;';
					
					updated_elements.push(element);
					
					a.style.cssFloat = 'left';
					a.lineobj = self_line;
					//self_line.words.push( a );
					if(element && element.lineobj.line == self_line.line) self_line.words.insertAfter(element, a);
					else self_line.words.push( a );
					
					element = a;
					/*
					 *	There is sometimes trouble under IE: the inserted elements being close together, line-breaking is disabled
					 *	which effectively removes any chance of having a wrapping editor. To avoid this, we create a 'text node'
					 *	between each, containing a single zero width breaking space, so that the spacing is not modified, but the 
					 *	breaking is! See http://unicode.org/charts/charindex3.html
					 *
					 *	Aditionally, there appears to be another bug that incorrectly gives an element's offset position when it
					 *	is just after a line break. A combination of breaking and non-breaking spaces appears to fix this!
					 *
					 */
					 if(window.isIE) {
						self_line.cell.appendChild(document.createTextNode('\u200B'));		// breaking zero-width space
						self_line.cell.appendChild(document.createTextNode('\u200B'));		// breaking zero-width space
						self_line.cell.appendChild(document.createTextNode('\uFEFF'));		// non-breaking zero-width space
					}
				}
				editor.caret.obj = element;
				editor.caret.objOffset = 0;
			}
			
			// First add the text to the existing span element
			if(w[i]) {
				if(!element.text) element.text = w[i];
				else {
					var a = element.text.slice(0, editor.caret.objOffset) || '';
					var b = element.text.slice(editor.caret.objOffset, element.text.length);
					element.text = a + w[i];
					if(b) w.push(b);
				}
				
				// Mark as needing a refresh!
				updated_elements.push(element);

				// Increment the caret position, if we are supposed to insert the text in front of it!
				if(i<original_length) editor.caret.objOffset += w[i].length;
			}
		}
		
		// Handle formatting changes, and propagate them...
		self_line.updateHighlight(updated_elements, true);
		
		self_line.updateFolding();
	}
	
	this.updateHighlight = function(up, destructive) {
		if(self_line.preventUpdate || !up.length) return;
		
		var element;
		var update;
		if(!destructive) {
			update = new Array(up.length);
			for(var i=0; i<up.length; i++) update[i] = up[i];
		}
		else update = up;
		
		var loop_count = 0;
		
		var f = function() {
			element = update.shift() || (element ? editor.getNextWord( element ) : null);
			if(!element) return;
			var k = element.text;
			if(!k) element.text = '';
			var before = editor.getPrevWord(element);
			element.before = before ? before.highlightState : null;
			var formatting_changed = Codespace.Formatter(
									element,
									(element == element.lineobj.words[ element.lineobj.words.length - 1]) ? true : false,
									editor.options.language);
			if(!k) element.text = k;
			// And stop when done...
			if(update.length || formatting_changed) {
				// DO some calculations, then pause and wait for the other tasks to catch up! (probably less important after 1000 words!)
				if(loop_count < 100) {loop_count++;f();}
				else {loop_count=0;setTimeout(f, 20);}
			}
		}
		
		f();
		/*while( element = (update.shift() || editor.getNextWord( element || self_line.words[0] )) ) {
			if(element.text == null) continue;
			var before = editor.getPrevWord(element);
			element.before = before ? before.highlightState : null;
			var formatting_changed = Codespace.Formatter(
									element,
									(element == element.lineobj.words[ element.lineobj.words.length - 1]) ? true : false,
									editor.options.language);
			// And stop when done...
			if(!update.length && !formatting_changed) break;
		}*/
	}

	
	/*
	 *	This implements click-positioning.
	 *	When a user clicks on the window, the cursor is positioned appropriately.
	 *	Also implements:
	 *		- double click: select word
	 *		- triple click: select line
	 *		- ctrl+click: select line
	 *		- shift+click: move selection instead of caret.
	 */
	this.onclick = function(evt, target) {
		/*
		 *	If only the row is clicked (not the content), we assume we want to go to the last line!
		 */
		var start_of_line = true;
		if(target.tagName.match(/td/i)) {
			if(target.className.match(/contentbar/i)) start_of_line = false;
			target = target.parentNode;
		}
		if(target.tagName.match(/tr/i)) {
			// TODO: we need a better "end of line" move!!!
			editor.selectCaret.move(self_line.line, start_of_line ? 0 : -1);
			if(!evt.shiftKey && !editor.process_mouse_up) editor.caret.move(self_line.line, start_of_line ? 0 : -1);
			return;
		}
		
		/*
		 *	Maybe we are clicking on the selection divs, in which case an equivalent target is given
		 */
		if(target.tagName.match(/div/i)) {
			// So either its the cursor: ignore it, let it go through
			if(target.target) target=target.target;		// Ain't this a beautiful line!!
			
			// Or its the actual selection, cancel it for now...
			else if(target.is_selection) {
				editor.selectCaret.obj = editor.caret.obj;
				editor.selectCaret.objOffset = editor.caret.objOffset;
			}
			else return;
		}
		
		/*
		 *	Otheriwse, we have clicked in the content, and we need to knwo where we are...
		 */
		if(target.tagName.match(/span/i)) {
			if(!self_line.clickTimes) self_line.clickTimes = new Array(3);
			if(!editor.process_mouse_up) {
				self_line.clickTimes[2] = self_line.clickTimes[1];
				self_line.clickTimes[1] = self_line.clickTimes[0];
				self_line.clickTimes[0] = {time: ((new Date()).getTime()), x: evt.pageX||evt.clientX, y: evt.pageY||evt.clientY};
			}
			
			/*
			 *	A double-click is only a double click if it is roughly near the location of the previous click.
			 */
			if(self_line.clickTimes[1] && (Math.abs(self_line.clickTimes[0].x-self_line.clickTimes[1].x) + Math.abs(self_line.clickTimes[0].y-self_line.clickTimes[1].y)) > 10) {
				if(self_line.clickTimes[2]) delete self_line.clickTimes[2];
				delete self_line.clickTimes[1];
			}
			
			
			/*
			 *	Handle a single click
			 */
			if((!self_line.clickTimes[1] || (self_line.clickTimes[0].time-self_line.clickTimes[1].time)>500) && !evt.ctrlKey) {
				// Get absolute coords of mouse click
				var mouse = {x:0, y:0};
				if(evt.pageX || evt.pageY) {mouse.x = evt.pageX;mouse.y = evt.pageY;}
				else if(evt.clientX || evt.clientY) {
					mouse.x = evt.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
					mouse.y = evt.clientY + document.body.scrollTop	+ document.documentElement.scrollTop;
				}
				
				var txt = target.text || '';
				var pos = getPosition(target);

				pos.x = mouse.x - pos.x;
				pos.y = mouse.y - pos.y;
				window.debug = target;
				var index = Math.round( pos.x*(txt.length) / pos.w );
				
				// Always move the selection cursor, but only move the normal caret if necessary...
				if(index == (target.text ? target.text.length : 0) && target.nextSibling) {target = target.nextSibling; index = 0;}
				editor.selectCaret.obj = target;
				editor.selectCaret.objOffset = index;
				if(!evt.shiftKey && !editor.process_mouse_up) {
					editor.caret.obj = target;
					editor.caret.objOffset = index;
				}
			}
			
			/*
			 *	Handle double click
			 */
			else if((!self_line.clickTimes[2] || (self_line.clickTimes[0].time-self_line.clickTimes[2].time)>700) && !evt.ctrlKey) {
				self.allow_dragging = false;
				
				editor.caret.obj = target;
				editor.caret.objOffset = 0;
				editor.selectCaret.obj = target;
				editor.selectCaret.objOffset = target.text.length;
			}
			
			/*
			 *	Handle triple click
			 */
			else if(self_line.clickTimes[2] || evt.ctrlKey) {
				self.allow_dragging = false;
				// If this is a hyperlink, open the link!
				if(target.text.match(/(https?\:\/\/|https?\:\/\/www\.|www\.|ftp\:\/\/)(.*)\.[a-z][a-z]([a-z]?)/i))
					window.open(target.text.match(/https?\:\/\//i) ? target.text : 'http://'+target.text);
				else {
					editor.caret.obj = self_line.words[0];
					editor.caret.objOffset = 0;
					editor.selectCaret.obj = self_line.words[self_line.words.length-1];					//(self_line.next ? self_line.next.words[0] : self_line.words[self_line.words.length-1]);
					editor.selectCaret.objOffset = self_line.words[self_line.words.length-1].text.length;	//(self_line.next ? 0 : self_line.words[self_line.words.length-1].text.length);
				}
			}

			// Otherwise reset click counter!
			else {
				self_line.clickTimes = new Array(3);
			}
		}
	}
	
	// And constructor stuff to ensure everything is initialised at the start...
	this.updateLineNumber();
	this.updateFolding();
	this.insertText(text);
}
